<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>日志管理</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        body {
            background: #f4f6fa;
            font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif;
            margin: 0;
            padding: 0;
        }
        .container {
            max-width: 900px;
            margin: 40px auto;
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 4px 24px rgba(0,0,0,0.08);
            padding: 32px 36px 24px 36px;
        }
        h2 {
            text-align: center;
            color: #2d3a4b;
            margin-bottom: 28px;
        }
        .search-bar {
            display: flex;
            justify-content: center;
            align-items: center;
            margin-bottom: 18px;
            flex-wrap: wrap;
        }
        #searchBox {
            width: 320px;
            padding: 8px 14px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            font-size: 15px;
            margin-right: 10px;
            transition: border 0.2s;
        }
        #searchBox:focus {
            border: 1.5px solid #409eff;
            outline: none;
        }
        .search-bar label {
            margin-right: 6px;
            color: #555;
            font-size: 15px;
        }
        .search-bar input[type="datetime-local"] {
            padding: 6px 8px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            font-size: 15px;
            margin-right: 6px;
        }
        .quick-range {
            margin-right: 10px;
        }
        .quick-range button {
            background: #e0e3e7;
            color: #333;
            border: none;
            border-radius: 6px;
            padding: 6px 12px;
            font-size: 14px;
            margin-right: 4px;
            cursor: pointer;
            transition: background 0.2s;
        }
        .quick-range button.selected, .quick-range button:hover {
            background: #409eff;
            color: #fff;
        }
        .search-bar button {
            background: #409eff;
            color: #fff;
            border: none;
            border-radius: 6px;
            padding: 8px 18px;
            font-size: 15px;
            margin-right: 8px;
            cursor: pointer;
            transition: background 0.2s;
        }
        .search-bar button:last-child {
            background: #e0e3e7;
            color: #333;
            margin-right: 0;
        }
        .search-bar button:hover {
            background: #66b1ff;
        }
        .search-bar button:last-child:hover {
            background: #cfd8dc;
        }
        .pagination {
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 18px 0 10px 0;
        }
        .pagination button {
            background: #fff;
            border: 1px solid #d1d5db;
            border-radius: 5px;
            padding: 5px 14px;
            margin: 0 4px;
            font-size: 15px;
            color: #409eff;
            cursor: pointer;
            transition: background 0.2s, color 0.2s;
        }
        .pagination button:hover {
            background: #409eff;
            color: #fff;
        }
        #pageInfo {
            font-size: 15px;
            color: #555;
            margin: 0 10px;
        }
        .console-wrapper {
            background: #181c20;
            border-radius: 8px;
            box-shadow: 0 1px 4px rgba(0,0,0,0.03);
            padding: 0;
            margin-top: 10px;
            margin-bottom: 10px;
        }
        #logConsole {
            font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', monospace;
            font-size: 14px;
            color: #e0e0e0;
            background: transparent;
            height: 420px;
            overflow-y: auto;
            padding: 18px 16px 18px 16px;
            white-space: pre-wrap;
            line-height: 1.7;
        }
        .log-info { color: #7fffd4; cursor: pointer; }
        .log-warn { color: #ffe082; cursor: pointer; }
        .log-error { color: #ff8a80; cursor: pointer; }
        .log-debug { color: #90caf9; cursor: pointer; }
        .log-default { color: #e0e0e0; cursor: pointer; }
        /* 浮窗样式 */
        .log-modal {
            position: fixed;
            left: 0; top: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.35);
            z-index: 9999;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .log-modal-content {
            background: #23272e;
            color: #e0e0e0;
            border-radius: 10px;
            min-width: 340px;
            max-width: 90vw;
            min-height: 180px;
            max-height: 80vh;
            padding: 28px 32px 18px 32px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.18);
            position: relative;
            font-size: 15px;
            overflow: auto;
        }
        .log-modal-content h3 {
            margin-top: 0;
            color: #7fffd4;
            font-size: 18px;
        }
        .log-modal-content table {
            width: 100%;
            border-collapse: collapse;
        }
        .log-modal-content td {
            padding: 4px 8px;
            border-bottom: 1px solid #333;
            color: #ffe082;
        }
        .log-modal-content td.key {
            color: #90caf9;
            font-weight: bold;
            width: 90px;
        }
        .log-modal-close {
            position: absolute;
            right: 18px;
            top: 12px;
            color: #aaa;
            font-size: 22px;
            cursor: pointer;
            transition: color 0.2s;
        }
        .log-modal-close:hover {
            color: #fff;
        }
        @media (max-width: 1200px) {
            .container { padding: 18px 4px; }
            #searchBox { width: 180px; }
        }
    </style>
</head>
<body>
<div class="top-nav">
    <div class="nav-item active" id="tab-log" onclick="switchTab('log')">日志管理</div>
    <div class="nav-item" id="tab-sys" onclick="switchTab('sys')">系统信息</div>
</div>
<div class="main-flex">
    <div class="log-charts-panel">
        <div class="log-charts-card">
            <div id="levelPie" style="width:320px;height:220px;margin-bottom:24px;"></div>
            <div id="countLine" style="width:320px;height:220px;"></div>
        </div>
    </div>
    <div class="container" id="logPanel">
        <h2>日志管理</h2>
        <div class="search-bar">
            <div class="level-group">
                <span>级别</span>
                <button class="level-btn" data-level="INFO" onclick="toggleLevel(this)">INFO</button>
                <button class="level-btn" data-level="WARN" onclick="toggleLevel(this)">WARN</button>
                <button class="level-btn" data-level="ERROR" onclick="toggleLevel(this)">ERROR</button>
                <button class="level-btn" data-level="DEBUG" onclick="toggleLevel(this)">DEBUG</button>
            </div>
            <input id="searchBox" type="text" placeholder="输入搜索条件，如 message LIKE \"aaa\"" />
            <label>时间从
                <input type="datetime-local" id="startTime" />
            </label>
            <label>到
                <input type="datetime-local" id="endTime" />
            </label>
            <span class="quick-range">
                <button onclick="setQuickRange('today')" id="btnToday">当日</button>
                <button onclick="setQuickRange('1d')" id="btn1d">最近一天</button>
                <button onclick="setQuickRange('1w')" id="btn1w">最近一周</button>
                <button onclick="setQuickRange('1m')" id="btn1m">最近一月</button>
            </span>
            <button onclick="doSearch()">搜索</button>
            <button onclick="clearSearch()">清空</button>
        </div>
        <div class="pagination">
            <button onclick="prevPage()">上一页</button>
            <span id="pageInfo"></span>
            <button onclick="nextPage()">下一页</button>
        </div>
        <div class="console-wrapper">
            <div id="logConsole"></div>
        </div>
    </div>
</div>
<div id="logModal" class="log-modal" style="display:none;">
    <div class="log-modal-content" id="logModalContent">
        <span class="log-modal-close" onclick="closeModal()">&times;</span>
        <h3>日志详情</h3>
        <table id="logDetailTable"></table>
    </div>
</div>
<style>
.main-flex {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    justify-content: flex-start;
    width: 100vw;
    min-height: 100vh;
    background: linear-gradient(120deg, #e0e7ef 0%, #f4f6fa 100%);
}
.log-charts-panel {
    width: 350px;
    min-width: 260px;
    max-width: 400px;
    background: none;
    box-shadow: none;
    margin: 48px 0 0 48px;
    padding: 0;
    font-size: 15px;
    height: fit-content;
    position: relative;
    overflow: visible;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.log-charts-card {
    width: 100%;
    background: #fff;
    border-radius: 18px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.13);
    padding: 32px 18px 24px 18px;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.level-group {
    display: flex;
    align-items: center;
    margin-right: 16px;
    gap: 4px;
}
.level-group span {
    color: #555;
    font-size: 15px;
    margin-right: 4px;
}
.level-btn {
    border: 1.5px solid #d1d5db;
    background: #fff;
    color: #409eff;
    border-radius: 16px;
    padding: 4px 16px;
    font-size: 14px;
    margin-right: 2px;
    cursor: pointer;
    transition: background 0.2s, color 0.2s, border 0.2s;
    outline: none;
}
.level-btn.selected {
    background: #409eff;
    color: #fff;
    border: 1.5px solid #409eff;
}
.level-btn:hover {
    background: #e3f2fd;
    color: #1976d2;
}
.log-charts {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.sysinfo-panel {
    width: 320px;
    min-width: 220px;
    max-width: 360px;
    background: linear-gradient(120deg, #23272e 80%, #2d3a4b 100%);
    color: #e0e0e0;
    border-radius: 18px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.13);
    margin: 48px 0 0 48px;
    padding: 32px 24px 24px 24px;
    font-size: 15px;
    height: fit-content;
    position: relative;
    overflow: hidden;
}
.sysinfo-panel h3 {
    color: #7fffd4;
    margin-top: 0;
    font-size: 22px;
    margin-bottom: 18px;
    text-align: center;
    letter-spacing: 2px;
}
.sysinfo-panel .sys-time {
    font-size: 20px;
    color: #ffe082;
    text-align: center;
    margin-bottom: 18px;
    font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', monospace;
    font-weight: bold;
}
.sysinfo-panel .sys-section {
    margin-bottom: 18px;
    padding-bottom: 10px;
    border-bottom: 1px solid #333a;
}
.sysinfo-panel .sys-section:last-child {
    border-bottom: none;
}
.sysinfo-panel .sys-label {
    color: #90caf9;
    font-weight: bold;
    width: 110px;
    display: inline-block;
    margin-right: 8px;
}
.sysinfo-panel .sys-value {
    color: #ffe082;
    font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'Menlo', monospace;
    font-size: 15px;
}
.sysinfo-panel .sys-bar {
    background: #333a;
    border-radius: 6px;
    height: 12px;
    width: 100%;
    margin: 4px 0 10px 0;
    overflow: hidden;
}
.sysinfo-panel .sys-bar-inner {
    height: 100%;
    border-radius: 6px;
    transition: width 0.5s;
}
.sysinfo-panel .bar-mem { background: linear-gradient(90deg, #7fffd4, #409eff); }
.sysinfo-panel .bar-cpu { background: linear-gradient(90deg, #ffe082, #ff8a80); }
.sysinfo-panel .bar-disk { background: linear-gradient(90deg, #90caf9, #7fffd4); }
.sysinfo-panel table {
    width: 100%;
    border-collapse: collapse;
}
.sysinfo-panel td {
    padding: 4px 6px;
    border-bottom: 1px solid #333;
    color: #ffe082;
    font-size: 14px;
}
.sysinfo-panel td.key {
    color: #90caf9;
    font-weight: bold;
    width: 90px;
}
.top-nav {
    width: 100vw;
    background: #23272e;
    display: flex;
    flex-direction: row;
    align-items: center;
    box-shadow: 0 2px 12px rgba(0,0,0,0.08);
    margin-bottom: 0;
    z-index: 10;
}
.nav-item {
    color: #90caf9;
    font-size: 18px;
    padding: 18px 38px 14px 38px;
    cursor: pointer;
    transition: color 0.2s, border-bottom 0.2s;
    border-bottom: 3px solid transparent;
    font-weight: 500;
    letter-spacing: 1px;
}
.nav-item.active {
    color: #7fffd4;
    border-bottom: 3px solid #7fffd4;
    background: #181c20;
}
@media (max-width: 900px) {
    .nav-item { font-size: 15px; padding: 12px 16px 10px 16px; }
}
@media (max-width: 1200px) {
    .main-flex { flex-direction: column; }
    .sysinfo-panel { margin: 18px auto 0 auto; }
    .log-charts-panel { margin: 18px auto 0 auto; }
}
</style>
<script>
let page = 1, size = 20, timer = null, lastQuery = '';
let lastLogs = [];
let quickRange = '';
let selectedLevels = [];

function fetchLogStats() {
    const query = buildQuery();
    fetch(`/api/logs/stats?query=${encodeURIComponent(query)}`)
        .then(r=>r.json())
        .then(drawLogCharts);
}
function drawLogCharts(data) {
    // 饼图
    let pie = echarts.init(document.getElementById('levelPie'));
    let pieData = (data.levelStats||[]).map(x=>({name:x.level,value:x.cnt}));
    pie.setOption({
        title:{text:'日志级别占比',left:'center',top:8,textStyle:{fontSize:15}},
        tooltip:{trigger:'item'},
        legend:{bottom:0},
        series:[{type:'pie',radius:['40%','70%'],data:pieData,label:{formatter:'{b}: {d}%'}}]
    });
    // 折线图（多级别）
    let line = echarts.init(document.getElementById('countLine'));
    let timeStats = data.timeStats || [];
    // 统计所有出现过的日志级别
    let allLevels = Array.from(new Set(timeStats.flatMap(x=>Object.keys(x)).filter(k=>k!=='hour')));
    let x = timeStats.map(x=>x.hour);
    let series = allLevels.map((level,idx)=>({
        name: level,
        type: 'line',
        data: timeStats.map(row=>row[level]||0),
        smooth: true,
        areaStyle: {},
        symbol: 'circle',
        emphasis: { focus: 'series' },
        lineStyle: { width: 2 },
    }));
    line.setOption({
        title:{text:'日志数量趋势',left:'center',top:8,textStyle:{fontSize:15}},
        tooltip:{trigger:'axis'},
        legend:{top:32},
        xAxis:{type:'category',data:x},
        yAxis:{type:'value'},
        series: series
    });
}
function fetchLogs(auto=false) {
    const query = buildQuery();
    lastQuery = query;
    fetch(`/api/logs?query=${encodeURIComponent(query)}&page=${page}&size=${size}`)
        .then(r => r.json())
        .then(data => {
            lastLogs = data.logs || [];
            renderConsole(lastLogs);
            document.getElementById('pageInfo').innerText = `第${data.page}页 / 共${Math.ceil((data.total||0)/size)}页 (共${data.total||0}条)`;
        });
    fetchLogStats();
    if (!auto) {
        if (timer) clearInterval(timer);
        timer = setInterval(() => fetchLogs(true), 2000);
    }
}
function buildQuery() {
    let q = document.getElementById('searchBox').value.trim();
    let start = document.getElementById('startTime').value;
    let end = document.getElementById('endTime').value;
    let timeCond = '';
    let levelCond = '';
    if(selectedLevels.length>0) {
        levelCond = `level IN (${selectedLevels.map(l=>`'${l}'`).join(',')})`;
    }
    if(start) {
        timeCond += `timestamp >= '${formatToClickhouseTime(start)}'`;
    }
    if(end) {
        if(timeCond) timeCond += ' AND ';
        timeCond += `timestamp <= '${formatToClickhouseTime(end)}'`;
    }
    let conds = [];
    if(q) conds.push(q);
    if(levelCond) conds.push(levelCond);
    if(timeCond) conds.push(timeCond);
    return conds.join(' AND ');
}
function formatToClickhouseTime(val) {
    // val: yyyy-MM-ddTHH:mm 或 yyyy-MM-ddTHH:mm:ss
    if(!val) return '';
    let [date, time] = val.split('T');
    if(!time) return date;
    if(time.length === 5) time = time + ':00'; // 补秒
    return date + ' ' + time;
}
function setQuickRange(type) {
    // 清除所有按钮选中状态
    ['btnToday','btn1d','btn1w','btn1m'].forEach(id=>document.getElementById(id).classList.remove('selected'));
    let now = new Date();
    let start, end;
    if(type==='today') {
        start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0,0,0);
        end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23,59,59);
        document.getElementById('btnToday').classList.add('selected');
    } else if(type==='1d') {
        start = new Date(now.getTime() - 24*60*60*1000);
        end = now;
        document.getElementById('btn1d').classList.add('selected');
    } else if(type==='1w') {
        start = new Date(now.getTime() - 7*24*60*60*1000);
        end = now;
        document.getElementById('btn1w').classList.add('selected');
    } else if(type==='1m') {
        start = new Date(now.getTime() - 30*24*60*60*1000);
        end = now;
        document.getElementById('btn1m').classList.add('selected');
    }
    document.getElementById('startTime').value = formatDateTimeLocal(start);
    document.getElementById('endTime').value = formatDateTimeLocal(end);
}
function formatDateTimeLocal(date) {
    if(!date) return '';
    let y = date.getFullYear();
    let m = (date.getMonth()+1).toString().padStart(2,'0');
    let d = date.getDate().toString().padStart(2,'0');
    let h = date.getHours().toString().padStart(2,'0');
    let min = date.getMinutes().toString().padStart(2,'0');
    let s = date.getSeconds().toString().padStart(2,'0');
    return `${y}-${m}-${d}T${h}:${min}:${s}`;
}
function renderConsole(logs) {
    const logConsole = document.getElementById('logConsole');
    logConsole.innerHTML = logs.map((log, idx) => {
        let level = (log.level||'').toUpperCase();
        let levelClass = 'log-default';
        if(level==='INFO') levelClass = 'log-info';
        else if(level==='WARN') levelClass = 'log-warn';
        else if(level==='ERROR') levelClass = 'log-error';
        else if(level==='DEBUG') levelClass = 'log-debug';
        return `<span class="${levelClass}" onclick="showLogDetail(${idx})">[${log.timestamp||''}] [${level}] ${log.message||''} (thread=${log.thread||''}, logger=${log.logger||''}, trace_id=${log.trace_id||''}, span_id=${log.span_id||''})</span>`;
    }).join('\n');
    logConsole.scrollTop = logConsole.scrollHeight;
}
function showLogDetail(idx) {
    const log = lastLogs[idx];
    const table = document.getElementById('logDetailTable');
    table.innerHTML = '';
    for(const key of ['timestamp','level','message','thread','logger','trace_id','span_id']) {
        table.innerHTML += `<tr><td class='key'>${key}</td><td>${log[key]||''}</td></tr>`;
    }
    document.getElementById('logModal').style.display = 'flex';
}
function closeModal() {
    document.getElementById('logModal').style.display = 'none';
}
function doSearch() { page = 1; fetchLogs(); }
function clearSearch() {
    document.getElementById('searchBox').value = '';
    document.getElementById('startTime').value = '';
    document.getElementById('endTime').value = '';
    selectedLevels = [];
    document.querySelectorAll('.level-btn').forEach(btn=>btn.classList.remove('selected'));
    ['btnToday','btn1d','btn1w','btn1m'].forEach(id=>document.getElementById(id).classList.remove('selected'));
    doSearch();
}
function prevPage() { if(page>1){ page--; fetchLogs(); } }
function nextPage() { page++; fetchLogs(); }
function toggleLevel(btn) {
    const level = btn.getAttribute('data-level');
    if(selectedLevels.includes(level)) {
        selectedLevels = selectedLevels.filter(l=>l!==level);
        btn.classList.remove('selected');
    } else {
        selectedLevels.push(level);
        btn.classList.add('selected');
    }
    doSearch();
}
function renderSysInfo(data) {
    const table = document.getElementById('sysInfoTable');
    let html = '';
    // 系统时间
    html += `<tr><td colspan='2' class='sys-time'>${data.systemTime ? formatDateTime(data.systemTime) : ''}</td></tr>`;
    // JVM
    html += `<tr class='sys-section'><td colspan='2'><span class='sys-label'>JVM</span> <span class='sys-value'>${data.jvmVersion||''} (${data.jvmVendor||''})</span></td></tr>`;
    html += `<tr><td class='sys-label'>启动时间</td><td class='sys-value'>${data.jvmStartTime ? formatDateTime(data.jvmStartTime) : ''}</td></tr>`;
    html += `<tr><td class='sys-label'>运行时长</td><td class='sys-value'>${data.jvmUptime||''}</td></tr>`;
    // 内存
    let heapUsed = parseFloat((data.heapUsed||'0').replace('MB',''));
    let heapMax = parseFloat((data.heapMax||'0').replace('MB',''));
    let memPercent = heapMax ? Math.round(heapUsed/heapMax*100) : 0;
    html += `<tr class='sys-section'><td class='sys-label'>内存</td><td class='sys-value'>${data.heapUsed||''} / ${data.heapMax||''} (${memPercent}%)</td></tr>`;
    html += `<tr><td colspan='2'><div class='sys-bar'><div class='sys-bar-inner bar-mem' style='width:${memPercent}%;'></div></div></td></tr>`;
    // CPU/线程
    html += `<tr class='sys-section'><td class='sys-label'>CPU核数</td><td class='sys-value'>${data.cpuCores||''}</td></tr>`;
    html += `<tr><td class='sys-label'>线程数</td><td class='sys-value'>${data.threadCount||''}</td></tr>`;
    // 磁盘
    let diskFree = parseFloat((data.diskFree||'0').replace('GB',''));
    let diskTotal = parseFloat((data.diskTotal||'0').replace('GB',''));
    let diskPercent = diskTotal ? Math.round((diskTotal-diskFree)/diskTotal*100) : 0;
    html += `<tr class='sys-section'><td class='sys-label'>磁盘</td><td class='sys-value'>${diskFree}GB / ${diskTotal}GB (已用${diskPercent}%)</td></tr>`;
    html += `<tr><td colspan='2'><div class='sys-bar'><div class='sys-bar-inner bar-disk' style='width:${diskPercent}%;'></div></div></td></tr>`;
    // OS
    html += `<tr class='sys-section'><td class='sys-label'>操作系统</td><td class='sys-value'>${data.osName||''} ${data.osVersion||''}</td></tr>`;
    table.innerHTML = html;
}
function formatDateTime(val) {
    if(!val) return '';
    let d = new Date(val);
    if(isNaN(d.getTime())) return val;
    return d.getFullYear()+'-'+(d.getMonth()+1).toString().padStart(2,'0')+'-'+d.getDate().toString().padStart(2,'0')+' '+d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');
}
function fetchSysInfo() {
    fetch('/api/sysinfo').then(r=>r.json()).then(renderSysInfo);
}
function switchTab(tab) {
    if(tab==='log') {
        document.getElementById('logPanel').style.display = '';
        document.getElementById('sysPanel').style.display = 'none';
        document.getElementById('tab-log').classList.add('active');
        document.getElementById('tab-sys').classList.remove('active');
    } else {
        document.getElementById('logPanel').style.display = 'none';
        document.getElementById('sysPanel').style.display = '';
        document.getElementById('tab-log').classList.remove('active');
        document.getElementById('tab-sys').classList.add('active');
    }
}
setInterval(fetchSysInfo, 2000);
window.onload = () => { fetchLogs(); fetchSysInfo(); switchTab('log'); };
</script>
</body>
</html> 